---
title: Analyzing the Utah Bikeways data
subtitle: Comparing the bikeways by type with the highway mileage for each city
description: This analysis calculates and compares roadway lane miles and bikeway facility miles across Utah municipalities using spatial data from the Utah Geospatial Resource Center (UGRC).
author:
- name: Pukar Bhandari
email: pukar.bhandari@wfrc.utah.gov
affiliation:
- name: Wasatch Front Regional Council
url: "https://wfrc.utah.gov/"
date: "2025-11-16"
---
# Introduction
This analysis examines the distribution of roadway and bikeway infrastructure across Utah municipalities. The primary objectives are to:
1. Calculate total road lane miles for each municipality
2. Calculate bikeway lane miles by facility type for each municipality
3. Compare roadway and bikeway infrastructure across municipalities
All spatial data is sourced from the Utah Geospatial Resource Center (UGRC) and processed using the R spatial ecosystem.
# Setup Environment
## Install Packages
The following packages are required for this analysis. Install them if not already available in your R environment.
```r
install.packages("dplyr")
install.packages("tidyr")
install.packages("readr")
install.packages("stringr")
install.packages("forcats")
install.packages("fs")
install.packages("sf")
install.packages("arcgislayers")
install.packages("janitor")
install.packages("units")
install.packages("tmap")
```
## Load Packages
```{r}
library(dplyr) # for data manipulation
library(tidyr) # for tidy data operations (pivot functions)
library(readr) # read and write tabular data
library(stringr) # manipulate strings
library(forcats) # process categorical data
library(fs) # for file system management
library(sf) # for simple feature geometries
library(arcgislayers) # to read arcgis rest services
library(janitor) # clean column names
library(units) # set and adjust units of measurement
library(tmap) # view sf objects
```
## Environment Variables
We use EPSG:3566 (NAD83(HARN) / Utah North) projection, which uses US survey feet as the base unit. This projection is appropriate for statewide analysis in Utah and ensures accurate distance calculations.
```{r}
# Set Project CRS
PROJECT_CRS = "EPSG:3566"
# Set tmap mode to interactive
tmap::tmap_mode("view")
```
# Read Data from UGRC
All spatial datasets are downloaded from UGRC's ArcGIS REST services. To improve performance and enable offline work, the data is cached locally as GeoDatabase files. The code checks if local copies exist before downloading.
```{r}
# set data folder
dir_data <- "_data"
fs::dir_create(dir_data) # Create a directory if it doesnt exist
```
## Utah City Boundaries
Municipal boundaries are used to aggregate roadway and bikeway data by city. These boundaries represent the legal limits of incorporated municipalities in Utah.
```{r}
# Define paths
path_ut_cities <- file.path(dir_data, "UGRC", "UtahMunicipalBoundaries.gdb")
# Download if not exists
if (!fs::file_exists(path_ut_cities)) {
# Read data using arcgis package
sf_ut_cities <- arcgislayers::arc_read(
"https://services1.arcgis.com/99lidPhWCzftIe9K/ArcGIS/rest/services/UtahMunicipalBoundaries/FeatureServer/0",
crs = PROJECT_CRS
) |> janitor::clean_names()
# Write to a GeoDatabase
sf_ut_cities |>
sf::st_write(path_ut_cities, layer = "UtahMunicipalBoundaries", append = FALSE)
} else {
# Read from local copy
sf_ut_cities <- sf::st_read(path_ut_cities) |>
sf::st_transform(PROJECT_CRS)
}
```
```{r}
#| eval: false
tmap::qtm(sf_ut_cities)
```
## Utah County Boundaries
County boundaries are used to aggregate roadway and bikeway data for unincorporated areas in Utah.
```{r}
# Define paths
path_ut_counties <- file.path(dir_data, "UGRC", "UtahCountyBoundaries.gdb")
# Download if not exists
if (!fs::file_exists(path_ut_counties)) {
# Read data using arcgis package
sf_ut_counties <- arcgislayers::arc_read(
"https://services1.arcgis.com/99lidPhWCzftIe9K/ArcGIS/rest/services/UtahCountyBoundaries/FeatureServer/0",
crs = PROJECT_CRS
) |> janitor::clean_names()
# Write to a GeoDatabase
sf_ut_counties |>
sf::st_write(path_ut_counties, layer = "UtahCountyBoundaries", append = FALSE)
} else {
# Read from local copy
sf_ut_counties <- sf::st_read(path_ut_counties) |>
sf::st_transform(PROJECT_CRS)
}
```
```{r}
#| eval: false
tmap::qtm(sf_ut_cities)
```
## Utah Roadways
The Utah Roads dataset contains all roadway centerlines statewide, including the number of through lanes for each road segment. This forms the basis for calculating road lane miles.
```{r}
# Define paths
path_ut_roads <- file.path(dir_data, "UGRC", "UtahRoads.gdb")
# Download if not exists
if (!fs::file_exists(path_ut_roads)) {
# Read data using arcgis package
sf_ut_roads <- arcgislayers::arc_read(
"https://services1.arcgis.com/99lidPhWCzftIe9K/ArcGIS/rest/services/UtahRoads/FeatureServer/0",
crs = PROJECT_CRS
) |> janitor::clean_names()
# Write to a GeoDatabase
sf_ut_roads |>
sf::st_write(path_ut_roads, layer = "UtahRoads", append = FALSE)
} else {
# Read from local copy
sf_ut_roads <- sf::st_read(path_ut_roads) |>
sf::st_transform(PROJECT_CRS)
}
```
```{r}
#| eval: false
tmap::qtm(sf_ut_roadways)
```
## Utah Bikeways
The Bikeways dataset contains all existing bicycle facilities in Utah. Each segment has two facility type attributes (`facility1` and `facility2`) representing the bicycle facility in each direction of travel.
```{r}
# Define paths
path_ut_bikeways <- file.path(dir_data, "UGRC", "Bikeways.gdb")
# Download if not exists
if (!fs::file_exists(path_ut_bikeways)) {
# Read data using arcgis package
sf_ut_bikeways <- arcgislayers::arc_read(
"https://services1.arcgis.com/99lidPhWCzftIe9K/ArcGIS/rest/services/Bikeways/FeatureServer/0",
crs = PROJECT_CRS
) |> janitor::clean_names()
# Write to a GeoDatabase
sf_ut_bikeways |>
sf::st_write(path_ut_bikeways, layer = "Bikeways", append = FALSE)
} else {
# Read from local copy
sf_ut_bikeways <- sf::st_read(path_ut_bikeways) |>
sf::st_transform(PROJECT_CRS)
}
```
```{r}
#| eval: false
tmap::qtm(sf_ut_bikeways)
```
# Road Lane Miles by Municipality
## Add Unincorporated Areas
```{r}
tmap::qtm(sf_ut_cities, col = "blue", fill = NULL, size = 1) +
tmap::qtm(sf_ut_counties, col = "red", fill = NULL, size = 2)
```
```{r}
# Create combined municipal boundaries including unincorporated areas
sf_ut_municipal <- dplyr::bind_rows(
# Add incorporated cities
sf_ut_cities |>
dplyr::select(Name = name),
# Add unincorporated county areas
sf_ut_counties |>
dplyr::mutate(
# Convert county name to proper case and add "(Unincorporated)"
Name = paste0(stringr::str_to_title(name), " County (Unincorporated)")
) |>
dplyr::select(Name) |>
# Remove areas that overlap with cities
sf::st_difference(
sf_ut_cities |>
sf::st_union()
)
)
# View the result
sf_ut_municipal
```
## Calculate Lane Miles
Road lane miles are calculated by multiplying the length of each road segment by the number of through lanes. For road segments where the through lane count is missing (NA), we assume 1 lane as a conservative estimate.
The calculation converts the geometry length from US survey feet to miles using the `units` package, which properly handles unit conversions and maintains unit awareness throughout the analysis.
```{r}
sf_ut_roads <- sf_ut_roads |>
dplyr::mutate(
# TODO: fix thrulanes data
dot_thrulanes = dplyr::if_else(is.na(dot_thrulanes), 1, dot_thrulanes),
miles = units::set_units(sf::st_length(`SHAPE`), "mile"),
lane_miles = units::set_units(dot_thrulanes * miles, "mile")
)
sf_ut_roads |>
dplyr::select(dot_thrulanes, miles, lane_miles) |>
head(20)
```
## Aggregate by Municipality
Using spatial join, we associate each road segment with its municipality and sum the lane miles. Roads that fall outside municipal boundaries will have NA for city_name.
```{r}
# Calculate total lane miles by municipality
road_miles_by_city <- sf_ut_roads |>
# Spatial join: attach each road segment to the city it falls within
sf::st_join(
sf_ut_municipal,
join = sf::st_intersects
) |>
# Group and summarize: total lane miles per city
dplyr::group_by(Name) |>
dplyr::summarize(
total_lane_miles = sum(lane_miles, na.rm = TRUE),
.groups = "drop"
) |>
# Output as a non-spatial table
sf::st_drop_geometry()
# View the results
road_miles_by_city |>
dplyr::arrange(dplyr::desc(total_lane_miles))
```
**Key Finding:** Salt Lake City has the highest road lane miles among Utah municipalities, followed by St. George and West Valley City. The NA category represents roads outside municipal boundaries (unincorporated areas, state highways between cities, etc.).
# Bikeway Miles by Municipality
## Reclassify Bikeway Types to Categories
We use `forcats` package to respect the hierarchy of the bicycle facilities.
```{r}
# Define the order of facility classes
facility_class_order <- c(
"Paved Path",
"Protected Bike Lane",
"Bike Lane",
"Marked Route",
"Unmarked Route",
"Other"
)
# Reclassify bike facilities into broader categories as factors
sf_ut_bikeways <- sf_ut_bikeways |>
dplyr::mutate(
facility1_class = dplyr::case_when(
facility1 == "Trail or Pathway" ~ "Paved Path",
facility1 %in% c(
"Cycle track, at-grade, protected with parking (1A)",
"Cycle track, protected with barrier (1B)",
"Cycle track, raised and curb separated (1C)"
) ~ "Protected Bike Lane",
facility1 %in% c(
"Buffered bike lane (2A)",
"Bike lane (2B)"
) ~ "Bike Lane",
facility1 %in% c(
"Marked shared roadway (3B)",
"Signed shared roadway (3C)"
) ~ "Marked Route",
facility1 == "Shoulder bikeway (3A)" ~ "Unmarked Route",
TRUE ~ "Other"
),
facility2_class = dplyr::case_when(
facility2 == "Trail or Pathway" ~ "Paved Path",
facility2 %in% c(
"Cycle track, at-grade, protected with parking (1A)",
"Cycle track, protected with barrier (1B)",
"Cycle track, raised and curb separated (1C)"
) ~ "Protected Bike Lane",
facility2 %in% c(
"Buffered bike lane (2A)",
"Bike lane (2B)"
) ~ "Bike Lane",
facility2 %in% c(
"Marked shared roadway (3B)",
"Signed shared roadway (3C)"
) ~ "Marked Route",
facility2 == "Shoulder bikeway (3A)" ~ "Unmarked Route",
TRUE ~ "Other"
),
# Convert to factors with proper ordering
facility1_class = factor(facility1_class, levels = facility_class_order),
facility2_class = factor(facility2_class, levels = facility_class_order)
)
```
## Calculate Bikeway Lane Miles by Facility Type
Bikeway facilities are directional - a single bikeway segment can have different facility types in each direction (`facility1` and `facility2`). For this analysis, we treat each direction as a separate lane, meaning a 1-mile bikeway segment with bike lanes in both directions counts as 2 lane-miles of bike lanes.
The `pivot_longer()` function stacks `facility1` and `facility2` into separate rows, effectively doubling the mileage for segments where both directions have facilities. We then group by municipality and facility type to calculate total lane miles for each combination.
```{r}
# Prepare bikeways with both directions as lanes
bikeway_miles_by_city <- sf_ut_bikeways |>
dplyr::filter(cartocode != "9") |> # Remove 'sidewalks' and 'virtual link' connections
dplyr::select( facility1_class, facility2_class) |>
# Add mileage to each bikeway segment
dplyr::mutate(
miles = units::set_units(sf::st_length(SHAPE), "mile")
) |>
sf::st_join(
sf_ut_municipal,
join = sf::st_intersects
) |>
# Drop geometry from dataframe for further processing
sf::st_drop_geometry() |>
# Treat facility1 and facility2 as separate lane directions
tidyr::pivot_longer(
cols = c(facility1_class, facility2_class),
values_to = "facility_class"
) |>
# Apply weighting: 1.0 for bidirectional (Paved Path), 0.5 for directional facilities
dplyr::mutate(
weighted_miles = dplyr::if_else(
facility_class == "Paved Path",
as.numeric(miles) * 1.0,
as.numeric(miles) * 0.5
)
) |>
# Summarize total lane miles by city and facility class
dplyr::group_by(Name, facility_class) |>
dplyr::summarize(
total_lane_miles = sum(as.numeric(weighted_miles), na.rm = TRUE),
.groups = "drop"
) |>
# Make facility class into columns
tidyr::pivot_wider(
names_from = facility_class,
values_from = total_lane_miles,
values_fill = 0
)
# Add total across all bike lane types
bikeway_miles_by_city <- bikeway_miles_by_city |>
dplyr::rowwise() |>
dplyr::mutate(
`All Biking Facilities` = sum(dplyr::c_across(!Name))
) |>
dplyr::ungroup()
# View the results
bikeway_miles_by_city |>
dplyr::arrange(dplyr::desc(`All Biking Facilities`))
```
**Key Finding:** Bikeway infrastructure varies significantly by facility type across municipalities. The dataset includes various facility types such as bike lanes, protected bike lanes, shared-use paths, and trails.
# Combine dataframes
## Create Multimodal Summary
The final step combines road lane miles and bikeway lane miles by facility type into a single dataset. This allows for direct comparison of roadway and bikeway infrastructure across municipalities.
Each row represents a municipality, with columns for total road lane miles followed by columns for each bikeway facility type. Cities with zero bikeway miles for a particular facility type will show 0 in that column.
```{r}
# Combine with roads
combined_miles_by_city <- road_miles_by_city |>
# Convert road lane miles to numeric
dplyr::mutate(`All Roads` = as.numeric(total_lane_miles)) |>
dplyr::select(Name, `All Roads`) |>
# Join with bikeway data
dplyr::right_join(bikeway_miles_by_city, by = "Name") |>
# Reorder columns: Name, bike categories, total bike, then road
dplyr::select(
Name,
dplyr::all_of(facility_class_order), # bike lane categories
- Other,
# `All Biking Facilities`,
`All Roads`
) |>
# Round all numeric columns to one decimal
dplyr::mutate(dplyr::across(where(is.numeric), \(x) round(x, 1)))
combined_miles_by_city
```
**Key Finding:** The ratio of bikeway lane miles to road lane miles varies considerably across Utah municipalities, indicating different levels of investment in bicycle infrastructure relative to overall roadway networks.
# Export Final results
The analysis results are exported as a CSV file for further use in reports, visualizations, or other analyses. The output directory is created automatically if it doesn't exist.
```{r}
# set data folder
dir_output <- "_output"
fs::dir_create(dir_output) # Create a directory if it doesnt exist
```
```{r}
combined_miles_by_city |>
readr::write_csv(
file.path(dir_output, "miles_by_city.csv"),
append = FALSE
)
```
::: {.callout-tip title="Download the output files:"}
[miles_by_city.csv](./_output/miles_by_city.csv)
:::
# Conclusion
This analysis provides a comprehensive inventory of roadway and bikeway infrastructure across Utah municipalities. The methodology is reproducible and can be updated as new data becomes available from UGRC. The spatial join approach ensures accurate attribution of infrastructure to municipalities, though users should note that roads and bikeways in unincorporated areas will appear under the NA category.